﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection;
using Group3.Framework.Extensions;
using Group3.Framework.Exceptions;
using Group3.MediaTool.Interface;
using System.IO;
using Microsoft.CSharp;
using Group3.MediaTool.Data.IO;

namespace Group3.MediaTool.Generator
{
    /// <summary>
    /// @Version 1.0
    /// @Author Pascal Senjic
    /// @Comment
    ///     Classbuilder generates C# class from object definitions.
    ///     The generated class will be available for the MediaTool via MEF.
    ///     http://msdn.microsoft.com/en-us/library/y2k85ax6.aspx
    /// </summary>
    public class ClassBuilder
    {
        /// <summary> 
        /// Define the compile unit to use for code generation.  
        /// </summary>
        private CodeCompileUnit csharpUnit;

        /// <summary>
        /// Represents the class, which will be generated
        /// </summary>
        private CodeTypeDeclaration mediaClass;

        public void BuildClass(IObjectDefinition definition)
        {
            csharpUnit = new CodeCompileUnit();
            #region Class Header
            string space = "\t\t\t";

            CodeNamespace namespce = new CodeNamespace("Group3.MediaTool.Core");
            namespce.Imports.Add(new CodeNamespaceImport("System"));
            namespce.Imports.Add(new CodeNamespaceImport("System.Text"));
            namespce.Imports.Add(new CodeNamespaceImport("System.Linq"));
            namespce.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
            namespce.Imports.Add(new CodeNamespaceImport("System.ComponentModel"));
            namespce.Imports.Add(new CodeNamespaceImport("System.ComponentModel.Composition"));
            namespce.Imports.Add(new CodeNamespaceImport("System.Threading.Tasks"));
            namespce.Imports.Add(new CodeNamespaceImport("Group3.Framework.Extensions"));
            namespce.Imports.Add(new CodeNamespaceImport("Group3.Framework.Exceptions"));
            namespce.Imports.Add(new CodeNamespaceImport("Group3.MediaTool.Interface"));
            namespce.Imports.Add(new CodeNamespaceImport("Group3.MediaTool.Provider")); ;
            namespce.Imports.Add(new CodeNamespaceImport("Group3.MediaTool.Core")); ;
            namespce.Imports.Add(new CodeNamespaceImport("Group3.MediaTool.Core.Reactive"));
            mediaClass = new CodeTypeDeclaration("E_" + definition.OrmName + " : MarshalByRefObject ,Group3.MediaTool.Interface.IObjectRelationModel, INotifyPropertyChanged,INotifyPropertyChanging, IObservable<Group3.MediaTool.Interface.IObjectRelationModel>");
            mediaClass.IsClass = true;
            mediaClass.TypeAttributes = TypeAttributes.Public | TypeAttributes.Sealed;
            // Declare a new code attribute
            CodeAttributeDeclaration codeAttrDecl = new CodeAttributeDeclaration(
                "Export",
                new CodeAttributeArgument(new CodeTypeOfExpression(typeof(IObjectRelationModel))));
            mediaClass.CustomAttributes.Add(codeAttrDecl);
            #endregion
            #region InterfaceMethods
            CodeMemberEvent propertyChangedEvent = new CodeMemberEvent();
            propertyChangedEvent.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            propertyChangedEvent.Name = "PropertyChanged";
            propertyChangedEvent.Type = new CodeTypeReference(typeof(System.ComponentModel.PropertyChangedEventHandler));

            CodeMemberMethod propertyChangedMethod = new CodeMemberMethod();
            propertyChangedMethod.Attributes = MemberAttributes.Private | MemberAttributes.Final;
            propertyChangedMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "propertyname"));
            propertyChangedMethod.Name = "RaisePropertyChanged";
            CodeSnippetStatement propertyChangedSnippet = new CodeSnippetStatement(space + "if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyname));\r\n"
            + space + "try" + "\r\n"
            + space + "{" + "\r\n"
            + space + "\t" + "foreach (var observer in this.observers)" + "\r\n"
            + space + "\t" + "{" + "\r\n"
            + space + "\t\t" + "observer.OnNext(this);" + "\r\n"
            + space + "\t" + "}" + "\r\n"
            + space + "}" + "\r\n"
            + space + "catch(Exception ex)" + "\r\n"
            + space + "{" + "\r\n"
            + space + "\t" + "foreach (var observer in this.observers)" + "\r\n"
            + space + "\t" + "{" + "\r\n"
            + space + "\t\t" + "observer.OnError(ex);" + "\r\n"
            + space + "\t" + "}" + "\r\n"
            + space + "}"
                );
            propertyChangedMethod.Statements.Add(propertyChangedSnippet);

            CodeMemberEvent propertyChangingEvent = new CodeMemberEvent();
            propertyChangingEvent.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            propertyChangingEvent.Name = "PropertyChanging";
            propertyChangingEvent.Type = new CodeTypeReference(typeof(System.ComponentModel.PropertyChangingEventHandler));

            CodeMemberMethod propertyChangingMethod = new CodeMemberMethod();
            propertyChangingMethod.Attributes = MemberAttributes.Private | MemberAttributes.Final;
            propertyChangingMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "propertyname"));
            propertyChangingMethod.Name = "RaisePropertyChanging";
            CodeSnippetStatement propertyChangingSnippet = new CodeSnippetStatement(space + "if (PropertyChanging != null) PropertyChanging(this, new PropertyChangingEventArgs(propertyname));");
            propertyChangingMethod.Statements.Add(propertyChangingSnippet);

            CodeMemberMethod subscribeMethod = new CodeMemberMethod();
            subscribeMethod.Name = "Subscribe";
            CodeParameterDeclarationExpression subscribeParameter = new CodeParameterDeclarationExpression(typeof(IObserver<Group3.MediaTool.Interface.IObjectRelationModel>), "observer");
            subscribeMethod.Parameters.Add(subscribeParameter);
            subscribeMethod.ReturnType = new CodeTypeReference(typeof(IDisposable));
            CodeSnippetStatement subscribesnippet = new CodeSnippetStatement(space + "if(this.observers != null && !this.observers.Contains(observer))\r\n"
                + space + "\t" + "((List<IObserver<IObjectRelationModel>>)observers).Add(observer);\r\n" +
                space + "return new EntityUnsubscriber(this.observers, observer);");
            subscribeMethod.Statements.Add(subscribesnippet);
            subscribeMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;


            CodeMemberMethod readAllMethod = new CodeMemberMethod();
            readAllMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            readAllMethod.ReturnType = new CodeTypeReference(typeof(IEnumerable<IObjectRelationModel>));
            readAllMethod.Name = "ReadAll";
            CodeSnippetStatement readAllsnippet = new CodeSnippetStatement(space + "return Dal.GetProvider().GetDataFromStore<"+"E_" + definition.OrmName +">();");
            readAllMethod.Statements.Add(readAllsnippet);

            CodeMemberMethod readMethod = new CodeMemberMethod();
            readMethod.ReturnType = new CodeTypeReference(typeof(IEnumerable<IObjectRelationModel>));
            readMethod.Name = "Read";
            readMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            CodeSnippetStatement readSnippet = new CodeSnippetStatement(space + "return Dal.GetProvider().GetDataFromStore<" + "E_" + definition.OrmName + ">(filterStmt);");
            readMethod.Statements.Add(readSnippet);
            CodeParameterDeclarationExpression readParameters = new CodeParameterDeclarationExpression(typeof(Predicate<IObjectRelationModel>), "filterStmt");
            readMethod.Parameters.Add(readParameters);

            CodeMemberMethod writeMethod = new CodeMemberMethod();
            writeMethod.Name = "Write";
            CodeSnippetStatement writesnippet = new CodeSnippetStatement(space + "Dal.GetProvider().SaveChanges(this);");
            writeMethod.Statements.Add(writesnippet);
            writeMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            mediaClass.Members.Add(propertyChangedEvent);
            mediaClass.Members.Add(propertyChangedMethod);
            mediaClass.Members.Add(propertyChangingEvent);
            mediaClass.Members.Add(propertyChangingMethod);
            mediaClass.Members.Add(subscribeMethod);
            mediaClass.Members.Add(readAllMethod);
            mediaClass.Members.Add(readMethod);
            mediaClass.Members.Add(writeMethod);
            #endregion
            #region Properties and fields. 
            CodeMemberField memberField = new CodeMemberField();
            memberField.Attributes = MemberAttributes.Private;
            memberField.Name = "observers";
            memberField.Type = new CodeTypeReference(typeof(IEnumerable<IObserver<IObjectRelationModel>>));
            memberField.InitExpression = new CodeObjectCreateExpression(
                                            new CodeTypeReference(typeof(List<IObserver<IObjectRelationModel>>)));
            mediaClass.Members.Add(memberField);
            definition.DataFields.Each(itm => AddProperties(itm));
            

            #endregion

            namespce.Types.Add(mediaClass);
            csharpUnit.Namespaces.Add(namespce);
            
            //if(!Directory.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Classes")))
            //    Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Classes"));
            //if (File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Classes\\E_" + definition.OrmName + ".cs")))
            //    File.Delete(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Classes\\E_" + definition.OrmName + ".cs"));

            //GenerateCSharpCode(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Classes\\E_" + definition.OrmName + ".cs"));
            //CompileCSharpCode(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Classes\\E_" + definition.OrmName + ".cs"), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Classes\\E_" + definition.OrmName + ".dll"));

            if (!Directory.Exists(Path.Combine(StaticConfig.pathDataDefinition, "Classes")))
                Directory.CreateDirectory(Path.Combine(StaticConfig.pathDataDefinition, "Classes"));
            if (File.Exists(Path.Combine(StaticConfig.pathDataDefinition, "Classes\\E_" + definition.OrmName + ".cs")))
                File.Delete(Path.Combine(StaticConfig.pathDataDefinition, "Classes\\E_" + definition.OrmName + ".cs"));

            GenerateCSharpCode(Path.Combine(StaticConfig.pathDataDefinition, "Classes\\E_" + definition.OrmName + ".cs"));
            CompileCSharpCode(Path.Combine(StaticConfig.pathDataDefinition, "Classes\\E_" + definition.OrmName + ".cs"), Path.Combine(StaticConfig.pathDataDefinition, "Classes\\E_" + definition.OrmName + ".dll"));

        }
        /// <summary>
        /// Adds the Properties from Objectdefinition to the Class
        /// </summary>
        /// <param name="itm">Fielddefinition for the Property</param>
        private void AddProperties(IFieldDefinition itm)
        {
            string space = "\t\t\t\t";
     
            // Declare the property.

            //Declare memberName
            string membername = itm.FieldName.ToLower();

            //Declare propertyName
            string propertyName = itm.FieldName.Substring(0, 1).ToUpper() + itm.FieldName.Substring(1).ToLower();

            CodeMemberField memberField = new CodeMemberField();
            memberField.Attributes = MemberAttributes.Private;
            memberField.Name = membername;

            CodeMemberProperty property = new CodeMemberProperty();
            property.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            property.Name = propertyName;
            property.HasGet = true;
            property.HasSet = true;
            property.Comments.Add(new CodeCommentStatement(
                "The " + itm.FieldName + " property for the object."));
            CodeAttributeArgument codeAttr = null; 

            switch(itm.ValueType)
            {
                case e_ValueType.Boolean:
                    codeAttr =
                        new CodeAttributeArgument(new CodePrimitiveExpression("Boolean"));
                    property.Type = new CodeTypeReference(typeof(System.Boolean?));
                    memberField.Type = new CodeTypeReference(typeof(Group3.MediaTool.Interface.IMemberValue<System.Boolean?>));
                    memberField.InitExpression = new CodeObjectCreateExpression(
                                                    new CodeTypeReference(typeof(Group3.MediaTool.Interface.NullableMemberValue<System.Boolean>)));
                    break;
                case e_ValueType.Decimal:
                    codeAttr =
                        new CodeAttributeArgument(new CodePrimitiveExpression("Decimal"));
                    property.Type = new CodeTypeReference(typeof(System.Double?));
                    memberField.Type = new CodeTypeReference(typeof(Group3.MediaTool.Interface.IMemberValue<System.Double?>));
                    memberField.InitExpression = new CodeObjectCreateExpression(
                                                    new CodeTypeReference(typeof(Group3.MediaTool.Interface.NullableMemberValue<System.Double>)));
                    break;
                case e_ValueType.Integer:
                    codeAttr =
                        new CodeAttributeArgument(new CodePrimitiveExpression("Integer"));
                    property.Type = new CodeTypeReference(typeof(System.Int32?));
                    memberField.Type = new CodeTypeReference(typeof(Group3.MediaTool.Interface.IMemberValue<System.Int32?>));
                    memberField.InitExpression = new CodeObjectCreateExpression(
                                                    new CodeTypeReference(typeof(Group3.MediaTool.Interface.NullableMemberValue<System.Int32>)));
                    break;
                case e_ValueType.Relation:
                    codeAttr =
                        new CodeAttributeArgument(new CodePrimitiveExpression("Relation"));
                    property.Type = new CodeTypeReference(typeof(IEnumerable<IObjectRelationModel>));
                    memberField.Type = new CodeTypeReference(typeof(Group3.MediaTool.Interface.IMemberValue<IEnumerable<IObjectRelationModel>>));
                    memberField.InitExpression = new CodeObjectCreateExpression(
                                                    new CodeTypeReference(typeof(Group3.MediaTool.Interface.MemberValue<IEnumerable<IObjectRelationModel>>)));


                    property.GetStatements.Add(new CodeSnippetStatement(string.Format(space + "if({0}.Current == null) {0}.Current = new List<IObjectRelationModel>(); return {0}.Current;", memberField.Name)));
                    property.SetStatements.Add(new CodeSnippetStatement(string.Format(space + "RaisePropertyChanging(\"{0}\"); if({1}.Current == null) {1}.Current = new List<IObjectRelationModel>(); this.{1}.Current = value; RaisePropertyChanged(\"{0}\");", property.Name, memberField.Name)));
                    break;
                case e_ValueType.Stream:
                    codeAttr =
                        new CodeAttributeArgument(new CodePrimitiveExpression("Stream"));
                    property.Type = new CodeTypeReference(typeof(System.Byte[]));
                    memberField.Type = new CodeTypeReference(typeof(Group3.MediaTool.Interface.IMemberValue<System.Byte[]>));
                    memberField.InitExpression = new CodeObjectCreateExpression(
                                                    new CodeTypeReference(typeof(Group3.MediaTool.Interface.MemberValue<System.Byte[]>)));
                    break;
                case e_ValueType.String:
                    codeAttr =
                        new CodeAttributeArgument(new CodePrimitiveExpression("String"));
                    property.Type = new CodeTypeReference(typeof(System.String));
                    memberField.Type = new CodeTypeReference(typeof(Group3.MediaTool.Interface.IMemberValue<System.String>));
                    memberField.InitExpression = new CodeObjectCreateExpression(
                                                    new CodeTypeReference(typeof(Group3.MediaTool.Interface.MemberValue<System.String>)));
                    break;
                case e_ValueType.Unknown:
                default:
                    throw new Framework.Exceptions.InvalidFormatException("Error");
            }
            property.CustomAttributes.Add(new CodeAttributeDeclaration("Group3.MediaTool.Interface.FieldAttribute", codeAttr));
            if (property.GetStatements.Count <= 0)
                property.GetStatements.Add(new CodeSnippetStatement(string.Format(space + "return {0}.Current;", memberField.Name))); //the ".Current" allows to work with the Interface IMemberValue<T>. For more information, look into the Interface IMemberValue<T>
            if (property.SetStatements.Count <= 0)
                property.SetStatements.Add(new CodeSnippetStatement(string.Format(space + "RaisePropertyChanging(\"{0}\"); this.{1}.Current = value; RaisePropertyChanged(\"{0}\");", property.Name, memberField.Name))); //new CodeAssignStatement(new CodeFieldReferenceExpression(
            //    new CodeThisReferenceExpression(), memberField.Name + ".Current"),
            //    new CodePropertySetValueReferenceExpression()));

            mediaClass.Members.Add(memberField);
            mediaClass.Members.Add(property);

        }

        /// <summary> 
        /// Generate CSharp source code from the compile unit. 
        /// </summary> 
        /// <param name="filename">Output file name</param>
        private void GenerateCSharpCode(string fileName)
        {
            CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
            CodeGeneratorOptions options = new CodeGeneratorOptions();
            options.BracingStyle = "C";
            using (StreamWriter sourceWriter = new StreamWriter(fileName))
            {
                provider.GenerateCodeFromCompileUnit(
                    csharpUnit, sourceWriter, options);
            }
        }

        /// <summary>
        /// Compiles the generated Class
        /// </summary>
        /// <param name="sourceFile">Classfile</param>
        /// <param name="dllFile">the Output dll</param>
        private void CompileCSharpCode(string sourceFile, string dllFile)
        {
            CSharpCodeProvider provider = new CSharpCodeProvider(
                new Dictionary<String, String> { { "CompilerVersion", "v4.0" } });
            
            // Build the parameters for source compilation.
            CompilerParameters cp = new CompilerParameters();

            // Add an assembly reference.
            cp.ReferencedAssemblies.Add("System.dll");
            cp.ReferencedAssemblies.Add("System.Core.dll");
            cp.ReferencedAssemblies.Add("System.Runtime.dll");
            cp.ReferencedAssemblies.Add("System.ComponentModel.Composition.dll");
            cp.ReferencedAssemblies.Add("Group3.Framework.dll");
            cp.ReferencedAssemblies.Add("Group3.MediaTool.Core.dll");
            cp.ReferencedAssemblies.Add("Group3.MediaTool.Generator.dll");
            cp.ReferencedAssemblies.Add("Group3.MediaTool.Interface.dll");
            cp.ReferencedAssemblies.Add("Group3.MediaTool.Provider.dll");

            // Generate an executable instead of 
            // a class library.
            cp.GenerateExecutable = false;

            // Set the assembly file name to generate.
            cp.OutputAssembly = dllFile;

            // Save the assembly as a physical file.
            cp.GenerateInMemory = false;

            // Invoke compilation.
            CompilerResults cr = provider.CompileAssemblyFromFile(cp, sourceFile);

            if (cr.Errors.Count > 0)
            {
                string errors = "";
                // Display compilation errors.
                errors = string.Format("Errors building {0} into {1}",
                    sourceFile, cr.PathToAssembly);
                foreach (CompilerError ce in cr.Errors)
                {
                    errors += string.Format("\r\n  {0}", ce.ToString());
                }
                throw new Exception(errors);
            }
        }


    }
}
